package com.myapps.controllers;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.method.extend.HandlerMethod;
import org.springframework.web.method.extend.HandlerMethodSelector;
import org.springframework.web.method.extend.RequestMappingInfo;


@Component
public class ServiceHandler<T> extends ApplicationObjectSupport implements InitializingBean {

    private boolean detectHandlerMethodsInAncestorContexts = false;
    private final Map<Integer, HandlerMethod> handlerMethods = new LinkedHashMap<Integer, HandlerMethod>();


    public void afterPropertiesSet() throws Exception {
        String[] beanNames =
                (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils
                    .beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class)
                        : getApplicationContext().getBeanNamesForType(Object.class));
        for (String beanName : beanNames) {
            if (isProcessor(getApplicationContext().getType(beanName))) {
                detectFuncMethod(beanName);
            }
        }
    }


    private void detectFuncMethod(final Object handler) {
        Class<?> handlerType =
                (handler instanceof String ? getApplicationContext().getType((String) handler) : handler
                    .getClass());

        // Avoid repeated calls to getMappingForMethod which would rebuild
        // RequestMatchingInfo instances
        final Map<Method, RequestMappingInfo> mappings = new IdentityHashMap<Method, RequestMappingInfo>();
        final Class<?> userType = ClassUtils.getUserClass(handlerType);
        // /============================================================
        Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
            public boolean matches(Method method) {
                RequestMappingInfo mapping = getMappingForMethod(method, userType);
                if (mapping != null) {
                    mappings.put(method, mapping);
                    return true;
                }
                else {
                    return false;
                }
            }
        });
        // /============================================================
        for (Method method : methods) {
            registerHandlerMethod(handler, method, mappings.get(method));
        }
    }


    private void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
        HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
        if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
            throw new IllegalStateException("Ambiguous mapping found. Cannot map '"
                    + newHandlerMethod.getBean() + "' bean method \n" + newHandlerMethod + "\nto " + mapping
                    + ": There is already '" + oldHandlerMethod.getBean() + "' bean method\n"
                    + oldHandlerMethod + " mapped.");
        }

        this.handlerMethods.put(mapping.value, newHandlerMethod);
        if (logger.isInfoEnabled()) {
            logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
        }
    }


    protected HandlerMethod createHandlerMethod(Object handler, Method method) {
        HandlerMethod handlerMethod;
        if (handler instanceof String) {
            String beanName = (String) handler;
            handlerMethod = new HandlerMethod(beanName, getApplicationContext(), method);
        }
        else {
            handlerMethod = new HandlerMethod(handler, method);
        }
        return handlerMethod;
    }


    private RequestMappingInfo getMappingForMethod(Method method, Class<?> userType) {
        RequestMappingInfo info = null;
        Function methodAnnotation = AnnotationUtils.findAnnotation(method, Function.class);
        if (methodAnnotation != null) {
            info = createRequestMappingInfo(methodAnnotation, null);
        }
        return info;
    }


    private RequestMappingInfo createRequestMappingInfo(Function methodAnnotation, Object object) {
        return new RequestMappingInfo(methodAnnotation.value());
    }


    private boolean isProcessor(Class<?> beanType) {
        return (AnnotationUtils.findAnnotation(beanType, Processor.class)) != null;
    }


    /**
     * Return a map with all handler methods and their mappings.
     */
    public Map<Integer, HandlerMethod> getHandlerMethods() {
        return Collections.unmodifiableMap(this.handlerMethods);
    }
}
